Skip to main content

Lazy Loading

Lazy Loading & Code Splitting in React: Dynamic Imports for Performance

1. Problem Statement

Case Study: EduStream Learning Platform

EduStream is a large online education platform:

  • It offers courses, quizzes, video lectures, forums, and a dashboard—all as separate features.

  • Most users only use a few features per session (e.g., watching a video or taking a quiz).

  • Loading the entire app upfront slows down the initial experience, especially on slow networks.

  • The team wants to load only the code needed for the current page or feature, and fetch the rest on demand.

Sample Image

The challenge:
How do you split your React app into smaller bundles and load them only when needed—improving initial load times, reducing bandwidth, and keeping the user experience smooth?


2. Learning Objectives

By the end of this tutorial, you will:

  • Understand what code splitting and lazy loading are, and why they matter for React apps.

  • Use dynamic import() and React.lazy() to load components only when needed.

  • Implement React.Suspense to handle loading states for lazy-loaded components.

  • Apply both route-based and component-based code splitting for maximum performance.

  • Understand best practices, pitfalls, and how to handle errors and loading states.


3. Concept Introduction with Analogy

Analogy: The Modular Classroom

  • Imagine EduStream as a school with many classrooms (features).

  • Instead of opening all classrooms at once (loading all code), you unlock each classroom only when a student needs it (lazy loading).

  • The janitor (React) keeps the main hall (core app) open, but unlocks classrooms (feature bundles) on demand, saving energy and time.


4. Technical Deep Dive

A. What is Code Splitting & Lazy Loading?

  • Code splitting breaks your app into smaller chunks (bundles) so users only download what they need.

  • Lazy loading means loading code (components, modules) only when it’s actually required, not upfront.

  • Dynamic imports (import()) and React.lazy() are the main tools for this in React.

Why?

  • Faster initial load: Only essential code is loaded first.

  • Better user experience: Users interact with the app sooner.

  • Efficient resource use: Less bandwidth, less memory, especially for large apps and slow networks.


B. Dynamic Imports: The Foundation

  • Use import() to load modules asynchronously.

  • Returns a promise that resolves to the module.

  • Can be used conditionally, in event handlers, or anywhere in your code—not just at the top.


// Only load the math module if needed
function handleCalculate() {
import('./math').then((math) => {
console.log(math.add(1, 2));
});
}
- This reduces the initial bundle size, as `math` is only loaded when `handleCalculate` is called .

C. React.lazy() and Suspense: Lazy Loading Components

1. React.lazy()

  • Wraps a dynamic import so you can use it as a component.

  • Loads the component only when it’s rendered for the first time.

import React, { Suspense } from 'react';

const VideoPlayer = React.lazy(() => import('./VideoPlayer'));

function App() {
return (
<div>
<h1>Welcome to EduStream</h1>
<Suspense fallback={<div>Loading video player...</div>}>
<VideoPlayer />
</Suspense>
</div>
);
}
  • Suspense provides a fallback UI while the component is loading.

2. Route-Based Code Splitting

  • Ideal for large, distinct sections (e.g., /courses, /dashboard, /forum).

  • Each route loads its own bundle.

import { BrowserRouter as Router, Routes, Route } from 'react-router-dom';
import { Suspense, lazy } from 'react';

const Dashboard = lazy(() => import('./Dashboard'));
const Courses = lazy(() => import('./Courses'));
const Forum = lazy(() => import('./Forum'));

function App() {
return (
<Router>
<Suspense fallback={<div>Loading page...</div>}>
<Routes>
<Route path="/" element={<Dashboard />} />
<Route path="/courses" element={<Courses />} />
<Route path="/forum" element={<Forum />} />
</Routes>
</Suspense>
</Router>
);
}
  • Only the code for the current route is loaded, reducing the initial bundle size.

3. Component-Based Code Splitting

  • Use for large, rarely-used, or non-essential components (e.g., a chart, modal, or admin panel).

  • Load the component only when needed, even within a page.

const Chart = React.lazy(() => import('./Chart'));

function AnalyticsPanel({ showChart }) {
return (
<div>
<h2>Analytics</h2>
{showChart && (
<Suspense fallback={<div>Loading chart...</div>}>
<Chart />
</Suspense>
)}
</div>
);
}
  • This keeps the main bundle lean, loading heavy components only when used.

D. Best Practices and Pitfalls

Best Practices:

  • Start with route-based splitting, then optimize further with component-based splitting.

  • Always provide a good fallback UI with Suspense to avoid blank screens.

  • Lazy load only non-critical components—keep headers, nav, and essential UI in the main bundle.

  • Use error boundaries to catch loading errors and show user-friendly messages.

  • Optimize chunk sizes with Webpack or Vite to avoid large lazy-loaded bundles.

Pitfalls:

  • Lazy loading too many small components can increase network requests and overhead.

  • Large lazy-loaded chunks can still cause delays—split wisely.

  • Forgetting Suspense fallback leads to blank screens while loading.

  • Dynamic import paths must be static strings (not variables) for bundlers to split correctly.


5. Step-by-Step Data Modeling & Code Walkthrough

A. Lazy Loading a Feature Component

import React, { Suspense } from 'react';

// Lazy load the Quiz component
const Quiz = React.lazy(() => import('./Quiz'));

function CoursePage() {
const [showQuiz, setShowQuiz] = React.useState(false);
return (
<div>
<h2>Course Content</h2>
<button onClick={() => setShowQuiz(true)}>Take Quiz</button>
{showQuiz && (
<Suspense fallback={<div>Loading quiz...</div>}>
<Quiz />
</Suspense>
)}
</div>
);
}
  • The Quiz code is only loaded when the user clicks "Take Quiz"

B. Route-Based Code Splitting Example

import { BrowserRouter as Router, Routes, Route } from 'react-router-dom';
import { Suspense, lazy } from 'react';

const VideoLecture = lazy(() => import('./VideoLecture'));
const Forum = lazy(() => import('./Forum'));

function EduStreamApp() {
return (
<Router>
<Suspense fallback={<div>Loading page...</div>}>
<Routes>
<Route path="/lecture/:id" element={<VideoLecture />} />
<Route path="/forum" element={<Forum />} />
</Routes>
</Suspense>
</Router>
);
}
  • Each page loads its own bundle, reducing initial load time.

C. Dynamic Import for Conditional Loading

function loadHelpWidget() {
import('./HelpWidget').then(({ default: HelpWidget }) => {
// Render or use HelpWidget as needed
});
}
  • Useful for loading third-party libraries, modals, or admin tools only when needed.

6. Interactive Challenge / Mini-Project

Your Turn!

  1. Create a ProfileSettings component that is only loaded when the user clicks a "Settings" button.

  2. Use React.lazy() and Suspense to load the component with a loading spinner.

  3. Add a route /admin that lazy-loads an AdminPanel component only when visited.

  4. Show how to handle loading errors with an error boundary.

7. Common Pitfalls & Best Practices

Common Pitfalls & Best Practices (Code Splitting & Lazy Loading)

PitfallBest Practice
Lazy loading too many small componentsFocus on big, rarely-used, or route-level chunks
No Suspense fallbackAlways wrap lazy components in Suspense
Large lazy-loaded chunksUse Webpack/Vite to optimize chunk sizes
Not handling loading errorsUse error boundaries for user-friendly errors
Dynamic import paths as variablesUse static strings for import() paths

9. Optional: Programmer’s Workflow Checklist

  • Use React.lazy() for dynamic imports of components.

  • Wrap all lazy components in Suspense with a good fallback UI.

  • Start with route-based code splitting, then optimize with component-based splitting.

  • Use error boundaries for robust error handling.

  • Test lazy loading on slow networks and devices.

  • Optimize chunk sizes and analyze bundles with Webpack/Vite tools.